Feature : Added text-based search feature#667
Feature : Added text-based search feature#667Isha-upadhyay wants to merge 4 commits intoAOSSIE-Org:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds end-to-end text-based image search: new DB search function and GET /images/search backend route; frontend API + hook; Redux split for text vs face searches; Navbar debounced text input and Home integration to show search results; face-search action renamed wiring updated. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Navbar as Navbar UI
participant Redux as Redux Store
participant Hook as useImageSearch
participant API as Frontend API
participant Backend as Backend API
participant DB as Database
participant Home as Home Component
User->>Navbar: Type query
Navbar->>Navbar: Debounce 500ms
Navbar->>Redux: dispatch startTextSearch(query)
Redux-->>Navbar: searchState updated (type='text', query)
Home->>Hook: useImageSearch(query, enabled=true)
Hook->>API: searchImages(query)
API->>Backend: GET /images/search?query=...
Backend->>DB: db_search_images(query, tagged?)
DB-->>Backend: aggregated image list
Backend-->>API: 200 OK with images
API-->>Hook: return results
Hook-->>Home: provide searchData
Home->>Redux: dispatch setImages(searchData)
Redux-->>Home: images updated
Home-->>User: Render "Search Results for 'query'"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)
64-76: Close button hidden during face search.The close button only appears when
isSearchActive(text search), but the query image is also displayed during face search. This creates inconsistent UX where users cannot close the query image during face search.Consider showing the close button whenever a query image exists:
- {isSearchActive && ( + {queryImage && ( <button onClick={() => { setSearchInput(''); dispatch(clearSearch()); }}Or adjust the condition to cover both search types:
- {isSearchActive && ( + {searchState.active && (frontend/src/pages/Home/Home.tsx (1)
50-64: Missing error handling for text search.The feedback handler only uses
isErroranderrorfrom the normal fetch query (lines 54-55), ignoring errors from the text search hook. If text search fails, users won't see any error feedback.Include text search error states:
+ const { + data: searchData, + isLoading: searchLoading, + isSuccess: searchSuccess, + isError: searchError, + error: searchErrorObj, + } = useImageSearch(searchQuery, isTextSearchActive); + // LOADING STATUS const finalLoading = isTextSearchActive ? searchLoading : isLoading; + const finalError = isTextSearchActive ? searchError : isError; + const finalErrorObj = isTextSearchActive ? searchErrorObj : error; // FEEDBACK HANDLER useMutationFeedback( { isPending: finalLoading, isSuccess: isTextSearchActive ? searchSuccess : isSuccess, - isError, - error, + isError: finalError, + error: finalErrorObj, },
🧹 Nitpick comments (5)
frontend/src/api/api-functions/images.ts (1)
17-24: Use endpoint constant and improve type safety.Two issues:
- The endpoint
/images/searchis hardcoded whilefetchAllImagesusesimagesEndpoints.getAllImages. Add a constant toapiEndpointsfor consistency.- Return type
Promise<any>loses type safety. Consider usingPromise<APIResponse>to matchfetchAllImages.+// In apiEndpoints.ts, add: +// searchImages: '/images/search', -export const searchImages = async (query: string, tagged?: boolean): Promise<any> => { +export const searchImages = async (query: string, tagged?: boolean): Promise<APIResponse> => { const params = new URLSearchParams({ query }); if (tagged !== undefined) { params.append('tagged', tagged.toString()); } - const response = await apiClient.get(`/images/search?${params.toString()}`); + const response = await apiClient.get<APIResponse>( + `${imagesEndpoints.searchImages}?${params.toString()}` + ); return response.data; };frontend/src/hooks/useImageSearch.ts (1)
4-10: Consider adding staleTime to reduce refetch frequency.The hook works correctly, but since search results are unlikely to change rapidly, adding
staleTimecould improve UX by reducing unnecessary API calls during typing (even with debouncing in the UI).export const useImageSearch = (query: string, enabled: boolean = true) => { return usePictoQuery({ queryKey: ['images', 'search', query], queryFn: () => searchImages(query), enabled: enabled && query.length > 0, + staleTime: 30000, // 30 seconds }); };backend/app/routes/images.py (2)
112-120: Remove redundant validation.This manual check is unnecessary since
Query(..., min_length=1)on line 98 already ensures the query is non-empty. FastAPI will return a 422 validation error before this code executes if the query is empty or whitespace-only (after strip).try: - if not query or not query.strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message="Search query cannot be empty", - ).model_dump(), - ) - images = db_search_images(query.strip(), tagged=tagged)
124-136: Metadata is being parsed twice.
db_search_images(inbackend/app/database/images.py, line 468) already callsimage_util_parse_metadata(metadata)and stores the parsed dict inimages_dict[image_id]["metadata"]. Calling it again here on line 130 will pass an already-parsed dict through the function.While
image_util_parse_metadatahandles dict input gracefully (returns it as-is), this is inefficient and potentially confusing. Either remove the call here or document the double-parse behavior.image_data = [ ImageData( id=image["id"], path=image["path"], folder_id=image["folder_id"], thumbnailPath=image["thumbnailPath"], - metadata=image_util_parse_metadata(image["metadata"]), + metadata=image["metadata"], # Already parsed in db_search_images isTagged=image["isTagged"], isFavourite=image.get("isFavourite", False), tags=image["tags"], ) for image in images ]backend/app/database/images.py (1)
455-465: Unusedlocation_namevariable.
location_nameis fetched from the query results (line 427, 464) but is never used in the function body. Either remove it from the SELECT clause or use it (e.g., for enriching search results).- for ( - image_id, - path, - folder_id, - thumbnail_path, - metadata, - is_tagged, - is_favourite, - tag_name, - location_name, - ) in results: + for ( + image_id, + path, + folder_id, + thumbnail_path, + metadata, + is_tagged, + is_favourite, + tag_name, + _location_name, # Used in WHERE clause only + ) in results:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
backend/app/database/images.py(2 hunks)backend/app/routes/images.py(2 hunks)docs/backend/backend_python/openapi.json(1 hunks)frontend/src/api/api-functions/images.ts(1 hunks)frontend/src/components/Dialog/FaceSearchDialog.tsx(2 hunks)frontend/src/components/Navigation/Navbar/Navbar.tsx(4 hunks)frontend/src/components/WebCam/WebCamComponent.tsx(2 hunks)frontend/src/features/searchSlice.ts(1 hunks)frontend/src/hooks/useImageSearch.ts(1 hunks)frontend/src/pages/Home/Home.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/hooks/useImageSearch.ts (2)
frontend/src/hooks/useQueryExtension.ts (1)
usePictoQuery(80-108)frontend/src/api/api-functions/images.ts (1)
searchImages(17-24)
frontend/src/components/WebCam/WebCamComponent.tsx (1)
frontend/src/features/searchSlice.ts (1)
startFaceSearch(29-34)
backend/app/routes/images.py (2)
backend/app/database/images.py (1)
db_search_images(399-499)backend/app/utils/images.py (1)
image_util_parse_metadata(496-513)
frontend/src/api/api-functions/images.ts (1)
frontend/src/api/axiosConfig.ts (1)
apiClient(5-12)
frontend/src/components/Dialog/FaceSearchDialog.tsx (1)
frontend/src/features/searchSlice.ts (1)
startFaceSearch(29-34)
frontend/src/pages/Home/Home.tsx (6)
frontend/src/features/imageSelectors.ts (1)
selectImages(5-7)frontend/src/hooks/useQueryExtension.ts (1)
usePictoQuery(80-108)frontend/src/api/api-functions/images.ts (1)
fetchAllImages(5-14)frontend/src/hooks/useImageSearch.ts (1)
useImageSearch(4-10)frontend/src/types/Media.ts (1)
Image(13-23)frontend/src/features/imageSlice.ts (1)
setImages(18-20)
backend/app/database/images.py (1)
backend/app/utils/images.py (1)
image_util_parse_metadata(496-513)
🔇 Additional comments (13)
frontend/src/features/searchSlice.ts (1)
1-45: LGTM! Clean separation of text and face search states.The refactored slice correctly distinguishes between text and face search modes. Each action properly clears the other search type's data, preventing stale state conflicts.
frontend/src/components/Dialog/FaceSearchDialog.tsx (2)
14-14: LGTM!Import correctly updated to use
startFaceSearchfor the face detection search flow.
86-86: LGTM!Dispatch correctly uses
startFaceSearchfor face-based image search, aligning with the refactored search slice.docs/backend/backend_python/openapi.json (1)
890-973: LGTM!The OpenAPI specification for the new
/images/searchendpoint is well-documented with proper parameter validation, response schemas, and a clear description of limitations (face clusters not supported).frontend/src/components/WebCam/WebCamComponent.tsx (2)
14-14: LGTM!Import correctly updated for the face search action.
82-82: LGTM!Correctly dispatches
startFaceSearchfor webcam-captured face searches, consistent with the refactored search slice.backend/app/database/images.py (2)
414-414: Consider escaping SQL LIKE wildcards in user input.The search pattern
f"%{query}%"doesn't escape%and_characters in the user's query. If a user searches for "100%", it will match "100" followed by any characters, not the literal "100%". For a search feature, this may be acceptable behavior, but it's worth documenting.If literal wildcard matching is desired, escape them:
def escape_like(query: str) -> str: return query.replace('%', '\\%').replace('_', '\\_') search_pattern = f"%{escape_like(query)}%"And add
ESCAPE '\\'to each LIKE clause in the query.
399-499: I encountered a repository cloning failure that prevents me from accessing the codebase to verify the reviewer's concern. Without access to the database schema files, I cannot definitively determine whether themetadatatable exists or validate the query's correctness.Given this limitation and the nature of the concern (a potential runtime failure if the table is missing), I must output the review comment with a verification tag indicating that manual investigation is needed:
Verify the
metadatatable exists in the database schema.The query joins
LEFT JOIN metadata md ON i.id = md.image_id, but the reviewer could not locate ametadatatable creation in thedb_create_images_tablefunction. If this table doesn't exist in the database schema, the query will fail at runtime.frontend/src/components/Navigation/Navbar/Navbar.tsx (2)
80-93: LGTM!The search input implementation is solid: controlled state, clear placeholder text, and good keyboard accessibility with Escape key support.
18-18: Good separation of text and face search states.The type-specific check properly distinguishes text search from face search, enabling different UI behaviors for each search mode.
frontend/src/pages/Home/Home.tsx (3)
26-30: Clean search state management.The type-specific flags (
isTextSearchActive,isFaceSearchActive) properly separate different search modes, and thesearchQueryfallback prevents undefined issues.
32-44: Proper query coordination.Disabling the normal fetch during active searches (line 36) prevents redundant API calls, and React Query will automatically refetch when the search clears. The text search hook is correctly gated by
isTextSearchActive.
86-90: Excellent user feedback with dynamic title.The title clearly communicates the search context and result count, providing immediate feedback to users about what they're viewing.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
frontend/src/components/Navigation/Navbar/Navbar.tsx (3)
16-21: Debounced effect is coupled to searchState and can be simplifiedRight now the effect depends on
searchState.active/searchState.typebut only uses them to gateclearSearch, which can:
- Dispatch
startTextSearchtwice per new query (once due tosearchInputchange, once afteractive/typeflip).- Auto‑clear a text search started elsewhere if
searchInputis still''whensearchStateflips to active.If the intent is “Navbar owns text search and
clearSearchis always safe when the input is empty”, you can decouple fromsearchStateentirely and dropisSearchActive:- const isSearchActive = searchState.active && searchState.type === 'text'; + // const isSearchActive = searchState.active && searchState.type === 'text'; const dispatch = useDispatch(); const [searchInput, setSearchInput] = useState(''); - // Debounce search - useEffect(() => { - const timer = setTimeout(() => { - if (searchInput.trim().length > 0) { - dispatch(startTextSearch(searchInput.trim())); - } else if (searchInput.trim().length === 0 && isSearchActive) { - dispatch(clearSearch()); - } - }, 500); - - return () => clearTimeout(timer); - - // use searchState - }, [searchInput, dispatch, searchState.active, searchState.type]); + // Debounce search based only on local input + useEffect(() => { + const trimmed = searchInput.trim(); + const timer = setTimeout(() => { + if (trimmed) { + dispatch(startTextSearch(trimmed)); + } else { + dispatch(clearSearch()); + } + }, 500); + return () => clearTimeout(timer); + }, [searchInput, dispatch]);This keeps behavior predictable (one dispatch per logical change, no hidden coupling to external text-search triggers).
Also applies to: 23-37
54-67: Consider whether Navbar should expose a way to clear the query imageThe query image preview looks good, and the
queryImage && ...guard keepsstartsWithsafe. However, with the inline close control removed, there’s no obvious way in the Navbar itself to “drop” the face/query image while staying on the same view.If FaceSearchDialog is the only place to clear it, that’s fine; otherwise, consider re‑adding a small close/clear affordance on the thumbnail so users can quickly revert to a pure text search.
69-81: Wire up the Search icon button or make it non-interactiveThe text input is already fully controlled and debounced, but the icon button is rendered as a
<button>with noonClick, so it appears interactive yet does nothing. That’s confusing and not great for accessibility.Either:
- Attach behavior (e.g., focus the input or immediately trigger a search), or
- Change it to a non-interactive element (e.g.,
<span>with appropriate classes andaria-hidden) so it’s clearly decorative.Also applies to: 84-93
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
frontend/src/components/Navigation/Navbar/Navbar.tsx(3 hunks)frontend/src/pages/Home/Home.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/pages/Home/Home.tsx
🔇 Additional comments (1)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)
1-4: Imports cleanly wire text search and icon usageThe new React/Redux imports plus
startTextSearch/clearSearchandSearchicon wiring are consistent with the rest of the file and the new text-search flow.Also applies to: 7-7
30d392e to
a477c36
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@backend/app/database/images.py`:
- Around line 417-437: The query in base_query incorrectly LEFT JOINs the
application-level metadata table (alias md) which has no image_id or location
columns, causing SQL errors; update the SELECT/WHERE to remove the LEFT JOIN
metadata md and instead search the JSON metadata stored in the images table
(images.metadata) directly (e.g., replace references to md.location with a
JSON/text search against images.metadata), leaving joins to image_classes (ic)
and mappings (m) intact; adjust the parameter bindings for the WHERE clause to
use images.metadata and ensure any selected fields no longer reference md or
md.location.
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx`:
- Around line 66-78: The Input component is missing the ref assignment so the
declared inputRef isn't wired and the focus button cannot focus the field; pass
the inputRef to the Input component via its ref prop (i.e., <Input
ref={inputRef} ...>) where the JSX currently renders Input, keeping the existing
props (type, placeholder, className, value, onChange, onKeyDown) intact so the
focus handler that references inputRef and the dispatch(clearSearch()) logic
continue to work.
In `@frontend/src/pages/Home/Home.tsx`:
- Around line 67-84: The useEffect in Home.tsx is missing reactive dependencies
causing stale runs; update the dependency array for the effect that dispatches
setImages so it includes all booleans and state used in the callback
(isTextSearchActive, searchSuccess, searchState.active, isSuccess) as well as
searchData, data and dispatch so the effect re-runs when those values change;
locate the useEffect block using the useEffect(...) that references
isTextSearchActive, searchSuccess, searchData, searchState.active, isSuccess,
data, dispatch and add the missing variables to its dependency list.
🧹 Nitpick comments (5)
frontend/src/hooks/useImageSearch.ts (1)
4-9: Consider exposing thetaggedfilter parameter.The
searchImagesAPI function supports an optionaltaggedparameter (seefrontend/src/api/api-functions/images.tslines 16-23), but this hook doesn't expose it. This limits search filtering capabilities.♻️ Proposed enhancement to support tagged filter
-export const useImageSearch = (query: string, enabled: boolean = true) => { +export const useImageSearch = (query: string, enabled: boolean = true, tagged?: boolean) => { return usePictoQuery({ - queryKey: ['images', 'search', query], - queryFn: () => searchImages(query), + queryKey: ['images', 'search', query, tagged], + queryFn: () => searchImages(query, tagged), enabled: enabled && query.length > 0, }); };backend/app/database/images.py (2)
455-465: Unused variablelocation_namein result unpacking.The
location_nameis unpacked from results (line 464) but never used in the image dictionary construction. Either use it or remove it from the SELECT and unpacking.♻️ Either use location_name or remove it
If keeping location search but not the separate variable:
for ( image_id, path, folder_id, thumbnail_path, metadata, is_tagged, is_favourite, tag_name, - location_name, + _, # location_name not needed separately ) in results:
414-414: Performance consideration: Leading wildcard prevents index usage.The search pattern
f"%{query}%"with leading wildcard will cause full table scans. For large image collections, this could be slow.Consider documenting this limitation or exploring alternatives like FTS (Full-Text Search) for future scalability, as mentioned in the PR objectives.
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)
21-34: Debounce effect looks correct but consider clearing search on unmount.The debounce implementation is clean. However, if the component unmounts while a search is active, the search state remains in Redux. Consider clearing search on unmount if that's the desired UX.
♻️ Optional: Clear search on unmount
useEffect(() => { const trimmed = searchInput.trim(); const timer = setTimeout(() => { if (trimmed) { dispatch(startTextSearch(trimmed)); } else { dispatch(clearSearch()); } }, 500); - return () => clearTimeout(timer); + return () => { + clearTimeout(timer); + }; }, [searchInput, dispatch]); + + // Optional: Clear search when navigating away + useEffect(() => { + return () => { + dispatch(clearSearch()); + }; + }, [dispatch]);frontend/src/pages/Home/Home.tsx (1)
80-83: Add Array.isArray validation for normal fetch data.The text search path (lines 70-74) validates that the data is an array, but the normal fetch path doesn't have the same validation.
♻️ Proposed fix for consistency
// No search → normal image fetch if (!searchState.active && isSuccess) { const images = (data?.data || []) as Image[]; + if (!Array.isArray(images)) { + console.error("Invalid data format"); + return; + } dispatch(setImages(images)); }
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@backend/app/database/images.py`:
- Around line 416-479: The current query (base_query) filters on m.name directly
which causes tag-only searches to return only the matching tag(s); replace this
with a two-step CTE approach: first build a CTE (e.g., matching_images) that
selects image ids matching the search criteria (use the same joins/WHERE logic
used now, including the optional i.isTagged filter) and then in the main query
select image columns and LEFT JOIN mappings/m to fetch all tags for those image
ids, ordering as before; update the SQL in base_query and the params list to
match the new CTE + main query structure while leaving the result-grouping logic
(the loop that builds images_dict and appends tag_name) unchanged so all tags
for matched images are returned.
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx`:
- Around line 84-88: In the Navbar component's icon-only focus button (the
button that calls inputRef.current?.focus()), add an accessible name by adding
aria-label="Focus search" and set type="button" to prevent accidental form
submissions; update the button element in Navbar.tsx accordingly so screen
readers receive a reliable label and the button won't submit forms.
- Around line 16-34: Replace the loose any type in the selector with the app
RootState to restore type safety: import RootState from '@/app/store' (same
module used elsewhere) and update the selector call that reads queryImage (the
useSelector around state.search.queryImage) to use RootState as the state type
parameter so it becomes useSelector((state: RootState) =>
state.search.queryImage); keep dispatch and other hooks unchanged.
🧹 Nitpick comments (1)
backend/app/database/images.py (1)
414-443: Upstream validation already prevents empty queries at the route level.The
search_imagesendpoint inbackend/app/routes/images.pyvalidates the query parameter withQuery(..., min_length=1)and explicitly checksif not query or not query.strip()before callingdb_search_images(). This guard prevents"%%"from reaching the database function.However, adding input validation at the database layer is still a reasonable defensive measure if the function might be called from other code paths outside the route. If the function is intended as an internal database utility only, the existing route-level validation is sufficient.
| # FIXED QUERY — removed face_clusters (they do NOT exist in DB) | ||
| base_query = """ | ||
| SELECT DISTINCT | ||
| i.id, | ||
| i.path, | ||
| i.folder_id, | ||
| i.thumbnailPath, | ||
| i.metadata, | ||
| i.isTagged, | ||
| i.isFavourite, | ||
| m.name as tag_name | ||
| FROM images i | ||
| LEFT JOIN image_classes ic ON i.id = ic.image_id | ||
| LEFT JOIN mappings m ON ic.class_id = m.class_id | ||
| WHERE ( | ||
| m.name LIKE ? OR | ||
| i.metadata LIKE ? OR | ||
| i.path LIKE ? | ||
| ) | ||
| """ | ||
|
|
||
| params = [search_pattern, search_pattern, search_pattern] | ||
|
|
||
| # Optional filter | ||
| if tagged is not None: | ||
| base_query += " AND i.isTagged = ?" | ||
| params.append(tagged) | ||
|
|
||
| base_query += " ORDER BY i.path, m.name" | ||
|
|
||
| cursor.execute(base_query, params) | ||
| results = cursor.fetchall() | ||
|
|
||
| # Group results into image format | ||
| images_dict = {} | ||
| from app.utils.images import image_util_parse_metadata | ||
|
|
||
| for ( | ||
| image_id, | ||
| path, | ||
| folder_id, | ||
| thumbnail_path, | ||
| metadata, | ||
| is_tagged, | ||
| is_favourite, | ||
| tag_name, | ||
| ) in results: | ||
|
|
||
| if image_id not in images_dict: | ||
| metadata_dict = image_util_parse_metadata(metadata) | ||
|
|
||
| images_dict[image_id] = { | ||
| "id": image_id, | ||
| "path": path, | ||
| "folder_id": str(folder_id), | ||
| "thumbnailPath": thumbnail_path, | ||
| "metadata": metadata_dict, | ||
| "isTagged": bool(is_tagged), | ||
| "isFavourite": bool(is_favourite), | ||
| "tags": [], | ||
| } | ||
|
|
||
| if tag_name and tag_name not in images_dict[image_id]["tags"]: | ||
| images_dict[image_id]["tags"].append(tag_name) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the file and examine the surrounding context
find . -type f -name "images.py" | grep -E "database|app" | head -5Repository: AOSSIE-Org/PictoPy
Length of output: 187
🏁 Script executed:
# Check for database schema files and migrations
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | head -20Repository: AOSSIE-Org/PictoPy
Length of output: 44
🏁 Script executed:
# Search for table definitions related to images, image_classes, mappings
rg -t py "CREATE TABLE|class.*images|class.*mappings" --no-heading -i | head -30Repository: AOSSIE-Org/PictoPy
Length of output: 2252
🏁 Script executed:
# Read the images table creation and the search function
cat -n backend/app/database/images.py | head -100Repository: AOSSIE-Org/PictoPy
Length of output: 3220
🏁 Script executed:
# Check the mappings table structure
cat -n backend/app/database/yolo_mapping.py | grep -A 20 "CREATE TABLE"Repository: AOSSIE-Org/PictoPy
Length of output: 785
🏁 Script executed:
# Read the complete search function around lines 416-479
sed -n '400,490p' backend/app/database/images.py | cat -nRepository: AOSSIE-Org/PictoPy
Length of output: 3310
🏁 Script executed:
# Let's also find the function name to understand context
sed -n '350,430p' backend/app/database/images.py | cat -nRepository: AOSSIE-Org/PictoPy
Length of output: 2898
Tag-only search results omit non-matching tags.
The WHERE clause filtering on m.name causes images found by tag search to return only the matching tag(s), while metadata/path matches return all tags because the WHERE conditions apply to the image level. This creates inconsistent tag lists depending on search type. Use a CTE to identify matching images first, then fetch all tags for those images:
Proposed fix (CTE to ensure complete tag lists)
- base_query = """
- SELECT DISTINCT
- i.id,
- i.path,
- i.folder_id,
- i.thumbnailPath,
- i.metadata,
- i.isTagged,
- i.isFavourite,
- m.name as tag_name
- FROM images i
- LEFT JOIN image_classes ic ON i.id = ic.image_id
- LEFT JOIN mappings m ON ic.class_id = m.class_id
- WHERE (
- m.name LIKE ? OR
- i.metadata LIKE ? OR
- i.path LIKE ?
- )
- """
+ base_query = """
+ WITH matched AS (
+ SELECT DISTINCT i.id
+ FROM images i
+ LEFT JOIN image_classes ic ON i.id = ic.image_id
+ LEFT JOIN mappings m ON ic.class_id = m.class_id
+ WHERE (
+ m.name LIKE ? OR
+ i.metadata LIKE ? OR
+ i.path LIKE ?
+ )
+ )
+ SELECT
+ i.id,
+ i.path,
+ i.folder_id,
+ i.thumbnailPath,
+ i.metadata,
+ i.isTagged,
+ i.isFavourite,
+ m.name as tag_name
+ FROM images i
+ JOIN matched mt ON i.id = mt.id
+ LEFT JOIN image_classes ic ON i.id = ic.image_id
+ LEFT JOIN mappings m ON ic.class_id = m.class_id
+ WHERE 1 = 1
+ """🤖 Prompt for AI Agents
In `@backend/app/database/images.py` around lines 416 - 479, The current query
(base_query) filters on m.name directly which causes tag-only searches to return
only the matching tag(s); replace this with a two-step CTE approach: first build
a CTE (e.g., matching_images) that selects image ids matching the search
criteria (use the same joins/WHERE logic used now, including the optional
i.isTagged filter) and then in the main query select image columns and LEFT JOIN
mappings/m to fetch all tags for those image ids, ordering as before; update the
SQL in base_query and the params list to match the new CTE + main query
structure while leaving the result-grouping logic (the loop that builds
images_dict and appends tag_name) unchanged so all tags for matched images are
returned.
| const queryImage = useSelector((state: any) => state.search.queryImage); | ||
| const dispatch = useDispatch(); | ||
| const [searchInput, setSearchInput] = useState(''); | ||
| const inputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const searchState = useSelector((state: any) => state.search); | ||
| const isSearchActive = searchState.active; | ||
| const queryImage = searchState.queryImage; | ||
| // Debounce search | ||
| useEffect(() => { | ||
| const trimmed = searchInput.trim(); | ||
|
|
||
| const timer = setTimeout(() => { | ||
| if (trimmed) { | ||
| dispatch(startTextSearch(trimmed)); | ||
| } else { | ||
| dispatch(clearSearch()); | ||
| } | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| }, [searchInput, dispatch]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd frontend && find . -type f \( -name "*.ts" -o -name "*.tsx" \) | head -20Repository: AOSSIE-Org/PictoPy
Length of output: 719
🏁 Script executed:
cd frontend && rg -l "RootState" --max-count=20Repository: AOSSIE-Org/PictoPy
Length of output: 532
🏁 Script executed:
cd frontend && rg -l "useAppSelector" --max-count=20Repository: AOSSIE-Org/PictoPy
Length of output: 44
🏁 Script executed:
cd frontend && find . -type f -name "store.ts" -o -name "store.js" -o -name "*store*" | grep -E "(store|redux|config)" | head -10Repository: AOSSIE-Org/PictoPy
Length of output: 80
🏁 Script executed:
cd frontend && rg -n "export.*RootState\|type RootState" -A 2 --max-count=5Repository: AOSSIE-Org/PictoPy
Length of output: 44
🏁 Script executed:
cat src/app/store.tsRepository: AOSSIE-Org/PictoPy
Length of output: 110
🏁 Script executed:
cd frontend && cat -n src/components/Navigation/Navbar/Navbar.tsx | sed -n '1,40p'Repository: AOSSIE-Org/PictoPy
Length of output: 1824
🏁 Script executed:
cd frontend && rg -A 3 "useSelector.*RootState" src/components --max-count=3Repository: AOSSIE-Org/PictoPy
Length of output: 385
🏁 Script executed:
cat frontend/src/app/store.tsRepository: AOSSIE-Org/PictoPy
Length of output: 1041
🏁 Script executed:
cd frontend && rg -n "import.*RootState" src/components/FaceCollections.tsxRepository: AOSSIE-Org/PictoPy
Length of output: 104
🏁 Script executed:
cd frontend && cat -n src/features/searchSlice.ts | head -50Repository: AOSSIE-Org/PictoPy
Length of output: 1505
Replace any with RootState in the search selector.
Line 16 uses state: any, which removes type safety and makes assumptions about the state shape fragile. The project exports RootState from @/app/store, which should be used here for consistency with other components like FaceCollections.tsx.
- const queryImage = useSelector((state: any) => state.search.queryImage);
+ const queryImage = useSelector((state: RootState) => state.search.queryImage);Add the import:
+import { RootState } from '@/app/store';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const queryImage = useSelector((state: any) => state.search.queryImage); | |
| const dispatch = useDispatch(); | |
| const [searchInput, setSearchInput] = useState(''); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| const searchState = useSelector((state: any) => state.search); | |
| const isSearchActive = searchState.active; | |
| const queryImage = searchState.queryImage; | |
| // Debounce search | |
| useEffect(() => { | |
| const trimmed = searchInput.trim(); | |
| const timer = setTimeout(() => { | |
| if (trimmed) { | |
| dispatch(startTextSearch(trimmed)); | |
| } else { | |
| dispatch(clearSearch()); | |
| } | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| }, [searchInput, dispatch]); | |
| const queryImage = useSelector((state: RootState) => state.search.queryImage); | |
| const dispatch = useDispatch(); | |
| const [searchInput, setSearchInput] = useState(''); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| // Debounce search | |
| useEffect(() => { | |
| const trimmed = searchInput.trim(); | |
| const timer = setTimeout(() => { | |
| if (trimmed) { | |
| dispatch(startTextSearch(trimmed)); | |
| } else { | |
| dispatch(clearSearch()); | |
| } | |
| }, 500); | |
| return () => clearTimeout(timer); | |
| }, [searchInput, dispatch]); |
🤖 Prompt for AI Agents
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx` around lines 16 - 34,
Replace the loose any type in the selector with the app RootState to restore
type safety: import RootState from '@/app/store' (same module used elsewhere)
and update the selector call that reads queryImage (the useSelector around
state.search.queryImage) to use RootState as the state type parameter so it
becomes useSelector((state: RootState) => state.search.queryImage); keep
dispatch and other hooks unchanged.
| <button | ||
| className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 cursor-pointer rounded-sm p-2" | ||
| title="Search" | ||
| aria-label="Search" | ||
| onClick={() => inputRef.current?.focus()} | ||
| className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2" | ||
| title="Focus search" | ||
| > |
There was a problem hiding this comment.
Add an accessible name to the icon-only focus button.
title isn’t reliably announced by screen readers; add aria-label (and set type="button" to avoid accidental form submits).
✅ Suggested fix
- <button
+ <button
+ type="button"
+ aria-label="Focus search"
onClick={() => inputRef.current?.focus()}
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2"
title="Focus search"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 cursor-pointer rounded-sm p-2" | |
| title="Search" | |
| aria-label="Search" | |
| onClick={() => inputRef.current?.focus()} | |
| className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2" | |
| title="Focus search" | |
| > | |
| <button | |
| type="button" | |
| aria-label="Focus search" | |
| onClick={() => inputRef.current?.focus()} | |
| className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2" | |
| title="Focus search" | |
| > |
🤖 Prompt for AI Agents
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx` around lines 84 - 88,
In the Navbar component's icon-only focus button (the button that calls
inputRef.current?.focus()), add an accessible name by adding aria-label="Focus
search" and set type="button" to prevent accidental form submissions; update the
button element in Navbar.tsx accordingly so screen readers receive a reliable
label and the button won't submit forms.
rohan-pandeyy
left a comment
There was a problem hiding this comment.
You've done a great job on this PR @Isha-upadhyay. There's one UX change I'd suggest you implement and it's that rather than applying a debounce functionality to the search feature, you should replace it with a "only dispatch when enter is pressed or search button is clicked" method. And incase a user clears out the text, (whether using the "x" button or clearing by backspace) it should automatically clear the search results.
There was a problem hiding this comment.
You can make use of RootState inside @/app/store.ts and replace the Debounce search with a search handler.
This PR introduces a complete text-based search system to enhance image discovery within PictoPy.
Previously, search was limited to face search only; the text search bar in the navbar was non-functional.
With this PR, users can now search images seamlessly using tags, filename, metadata, and even month/year.
Demo: https://www.loom.com/share/bd608939c716492abf8db5265671e5ed
What’s Added-
fixes #661
Summary by CodeRabbit
New Features
Refactor
Bug Fixes